java folder structure
contents
사실 자바(JVM) 자체는 폴더 구조에 대해 아주 "멍청(dumb)"합니다. 자바는 "폴더 A"가 "폴더 B"의 형제라는 사실을 본능적으로 알지 못합니다.
구조를 감지하고 프로젝트들을 연결하는 마법은 자바 언어가 아니라 빌드 도구(Build Tools, Maven 또는 Gradle) 를 통해 일어납니다.
다음은 자바의 멀티 모듈 프로젝트가 작동하는 방식에 대한 상세 분석입니다.
1. 고수준 관점: "리액터 (The Reactor)"
자동차를 만든다고 상상해 봅시다. 엔진 부서, 바퀴 부서, 차체 부서가 따로 있습니다.
- Java (JVM): 조립 라인의 노동자입니다. 부품을 주면 조립만 합니다. 순서는 모릅니다.
- Maven/Gradle: 공장 관리자입니다. 엔진이 먼저 만들어져야 차체에 넣을 수 있다는 순서를 알고 있습니다.
이 논리를 빌드 리액터(Build Reactor) 라고 부릅니다.
2. 물리적 구조 (디스크상의 모습)
일반적인 멀티 모듈 설정(기업 표준인 Maven을 예로 듭니다)에서 폴더 구조는 다음과 같습니다.
my-backend-system (루트 프로젝트)
├── pom.xml (부모 POM)
│
├── common-utils (하위 모듈 1: 공통 유틸리티)
│ ├── src/main/java...
│ └── pom.xml
│
├── user-service (하위 모듈 2: 사용자 서비스)
│ ├── src/main/java...
│ └── pom.xml
│
└── payment-service (하위 모듈 3: 결제 서비스)
├── src/main/java...
└── pom.xml
3. 1단계: 감지 (부모 POM)
어떻게 자바/Maven은 common-utils 폴더가 my-backend-system의 일부라는 것을 알까요?
부모 pom.xml이 지도가 됩니다. <modules> 태그를 사용하여 어떤 폴더를 들여다봐야 하는지 명시적으로 알려줍니다.
파일: my-backend-system/pom.xml
com.example
my-backend-system
1.0.0
pom
common-utils
user-service
payment-service
- 감지 메커니즘: 루트 폴더에서
mvn clean install을 실행하면, Maven은 이 리스트를 읽습니다. 그리고common-utils라는 폴더를 찾고, 그 안에pom.xml이 있는지 확인합니다. 만약 폴더는 있는데pom.xml이 없다면 빌드는 실패합니다.
4. 2단계: 연결 (의존성 관리)
그렇다면 user-service는 어떻게 common-utils 안에 있는 코드를 가져다 쓸까요?
단순히 import com.example.utils.StringHelper라고 쓴다고 되는 게 아닙니다. 의존성 연결(Dependency Link) 을 확립해야 합니다.
파일: user-service/pom.xml
com.example
my-backend-system
1.0.0
user-service
com.example
common-utils
1.0.0
기술적으로 일어나는 일:
- 1단계 (Common-Utils): Maven이
common-utils를 컴파일합니다..java파일들을.class로 바꾸고common-utils-1.0.0.jar라는 압축 파일로 만듭니다. - 2단계 (Install): Maven은 이 JAR 파일을 당신 컴퓨터의 로컬 저장소(Local Repository)(보통
~/.m2/repository에 숨겨진 폴더)에 복사해 둡니다. - 3단계 (User-Service):
user-service가 컴파일될 때, Maven은 로컬 저장소를 뒤져서common-utilsJAR를 찾아내고, 이를user-service의 클래스패스(Classpath) 에 추가합니다.
5. 3단계: 빌드 리액터 (그래프 해결)
이 부분이 가장 정교한 부분입니다.
루트 폴더에서 mvn compile을 입력하면, Maven은 단순히 알파벳 순서(Common, Payment, User)대로 빌드하지 않습니다. 의존성 그래프(Dependency Graph) 를 그립니다.
user-service는common-utils가 필요하다는 것을 봅니다.payment-service는user-service가 필요하다는 것을 봅니다.- 결과: 위상 정렬(Topological Sort) 을 계산하여 순서를 정합니다.
실행 순서:
common-utils $\rightarrow$ user-service $\rightarrow$ payment-service.
만약 순환 의존성(Circular Dependency)(A가 B를 필요로 하는데, B가 다시 A를 필요로 함)이 발생하면, 리액터는 이를 감지하고 즉시 에러를 띄우며 빌드를 중단합니다.
6. 내부 동작: 클래스패스 (런타임)
코드가 컴파일되고 실행되는 시점(Runtime)이 되면, "프로젝트"나 "모듈"이라는 개념은 사라집니다. JVM은 오직 클래스패스(Classpath) 만 이해합니다.
애플리케이션을 실행할 때, 실제 명령어는 다음과 같은 모습이 됩니다 (단순화함):
java -cp common-utils.jar:user-service.jar:spring-boot.jar com.example.user.Main
JVM은 여러분의 아름다운 계층형 폴더 구조를 납작하고 긴 JAR 파일 목록으로 만들어버립니다. user-service의 코드에서 import com.example.common.Utils를 호출하면, JVM은 목록에 있는 모든 JAR를 뒤져서 해당 클래스를 찾습니다.
7. 현대적 자바: JPMS (Java Platform Module System)
Java 9부터는 빌드 도구가 아니라 언어 자체에서 구조를 강제하는 방법이 도입되었습니다. 바로 module-info.java입니다.
파일: common-utils/src/main/java/module-info.java
module com.example.common {
exports com.example.common.utils; // 이 패키지만 외부 노출
// 다른 내부 패키지들은 효과적으로 "숨겨짐"
}
파일: user-service/src/main/java/module-info.java
module com.example.user {
requires com.example.common; // 명시적으로 이 모듈이 필요하다고 선언
}
- 차이점: Maven은 JAR 파일들을 물리적으로 연결해 줍니다. JPMS는 접근 권한(가시성)을 논리적으로 제어합니다. (예: JAR 파일은 가지고 있어도,
exports되지 않은 클래스는 코드에서 사용할 수 없음).
백엔드 개발자를 위한 요약
- 감지 (Detection): 루트
pom.xml의<modules>태그가 담당합니다. - 연결 (Connection): 하위 프로젝트의
<dependency>선언이 담당합니다. - 순서 정렬 (Ordering): 리액터(Reactor) 가 의존성을 분석하여 올바른 빌드 순서를 계산합니다.
- 런타임 (Runtime): JVM은 모든 구조를 클래스패스(JAR 목록) 로 납작하게 만들어 실행합니다.
references